/*
 * Copyright (C) 2012  Romain DUBOIS
 * 
 * This file is part of NeMoS - Network and Modular Software
 * 
 * NeMoS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * NeMoS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with NeMoS.  If not, see <http://www.gnu.org/licenses/>.
 */
package nemos;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.ServiceLoader;
import java.util.StringTokenizer;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.launch.Framework;
import org.osgi.framework.launch.FrameworkFactory;

/**
 * Launcher : The NeMoS main launcher.
 * 
 * <p>
 * This class launches the OSGi-Framework for NeMoS projects.
 * </p>
 * 
 * <p>
 * The main steps are:
 * <ol>
 * <li>Load external properties. Use system property
 * <code>nemos.conf.path</code> to locate the settings file. If no file is
 * found, read file <code>conf/config.properties</code>.</li>
 * <li>Load system properties (only OSGi properties are kept).</li>
 * <li>Define the default locale (use <code>nemos.osgi.locale</code> property).</li>
 * <li>Register a shutdown hook in order to close the framework properly.</li>
 * <li>Look for a framework and initialize it.</li>
 * <li>Install bundles located in the directories defined in the
 * <code>nemos.osgi.bundles.dirs</code> property.</li>
 * <li>Start all installed bundles in the same order.</li>
 * <li>Wait for the framework to stop.</li>
 * </ol>
 * </p>
 * 
 * @author Romain DUBOIS
 */
public class Launcher {

    /** Config file error message */
    private static final String CONFIG_FILE_ERROR = "Unable to load config file : "; //$NON-NLS-1$
    /** JAR file extension */
    private static final String JAR_EXTENSION = ".jar"; //$NON-NLS-1$
    /** No-available-Framework message */
    private static final String NO_OSGI_FRAMEWORK_FOUND = "No OSGi framework found"; //$NON-NLS-1$
    /** The OSGi properties root names */
    private static final String OSGI_PROP_NAME_ROOT = "org.osgi.framework."; //$NON-NLS-1$
    /** The shutdown thread name */
    private static final String SHUTDOWN_THREAD_NAME = "Shutdown Hook"; //$NON-NLS-1$
    /** Starting bundle error message */
    private static final String START_BUNDLE_ERROR = "An error occured while starting bundle"; //$NON-NLS-1$
    /** Starting error message */
    private static final String START_ERROR = "An error occured while starting framework"; //$NON-NLS-1$
    /** Stopping error message */
    private static final String STOP_ERROR = "An error occured while stopping framework"; //$NON-NLS-1$

    /**
     * The NeMoS entry point.
     * 
     * @param pArgs
     *            The command-line arguments (unused)
     */
    public static void main(final String[] pArgs) {
        System.exit(new Launcher().launch());
    }

    /** The launched framework */
    private Framework mFramework;

    /**
     * Constructor
     */
    public Launcher() {
        super();
    }

    /**
     * Launch NeMoS platform.
     * 
     * @return The exit code
     */
    public int launch() {
        // Load external properties
        final Properties lConfiguration = loadExternalProperties();

        // Load system properties
        loadSystemProperties(lConfiguration);

        // Define the default locale
        defineLocale(EProperties.LOCALE.getValue(lConfiguration));

        // Register a shutdown hook
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            @Override
            public void run() {
                stopFramework();
            }
        }, SHUTDOWN_THREAD_NAME));

        try {
            // Look for a framework and initialize it
            if (initFramework(lConfiguration)) {

                // Install bundles
                final List<Bundle> lBundles = installBundles(EProperties.BUNDLES_DIRS
                        .getValue(lConfiguration));

                // Start bundles and framework
                startBundles(lBundles);
                mFramework.start();

                // Wait for the framework to stop
                mFramework.waitForStop(0);
                return 0;
            }

            System.err.println(NO_OSGI_FRAMEWORK_FOUND);

        } catch (final Throwable t) {
            System.err.println(START_ERROR);
            t.printStackTrace();
        }
        return -1;
    }

    /**
     * Stop the framework.
     */
    void stopFramework() {
        try {
            if (mFramework != null && mFramework.getState() == Bundle.ACTIVE) {
                mFramework.stop();
                mFramework.waitForStop(0);
            }
        } catch (final Exception ex) {
            System.err.println(STOP_ERROR);
            ex.printStackTrace();
        }
    }

    /**
     * Set the default locale.
     * 
     * @param pLocaleName
     *            The locale name
     */
    private void defineLocale(final String pLocaleName) {
        if (pLocaleName != null && !pLocaleName.trim().isEmpty()) {
            final StringTokenizer lTokenikzer = new StringTokenizer(pLocaleName.trim(), "_"); //$NON-NLS-1$
            final Map<String, Map<String, Locale>> lLocales = new HashMap<String, Map<String, Locale>>();

            // Filter on language
            final String lLang = lTokenikzer.nextToken();
            for (final Locale lLocale : Locale.getAvailableLocales()) {

                if (lLang.equalsIgnoreCase(lLocale.getLanguage())) {
                    Map<String, Locale> lVariants = lLocales.get(lLocale.getCountry());
                    if (lVariants == null) {
                        lVariants = new HashMap<String, Locale>();
                        lLocales.put(lLocale.getCountry().toLowerCase(), lVariants);
                    }
                    lVariants.put(lLocale.getVariant().toLowerCase(), lLocale);
                }
            }

            // Language may be unknown
            if (!lLocales.isEmpty()) {

                // Then, compare country
                String lCountry = ""; //$NON-NLS-1$
                if (lTokenikzer.hasMoreTokens()) {
                    lCountry = lTokenikzer.nextToken().toLowerCase();
                }

                // Does country exist?
                Map<String, Locale> lVariants = lLocales.get(lCountry);
                if (lVariants == null) {
                    // Use the raw language
                    lVariants = lLocales.get(""); //$NON-NLS-1$
                }

                // Finally : variant
                String lVariant = ""; //$NON-NLS-1$
                if (lTokenikzer.hasMoreTokens()) {
                    lVariant = lTokenikzer.nextToken().toLowerCase();
                }

                // Does variant exist?
                Locale lLocale = lVariants.get(lVariant);
                if (lLocale == null) {
                    lLocale = lVariants.get(""); //$NON-NLS-1$
                }
                if (lLocale != null) {
                    Locale.setDefault(lLocale);
                }
            }
        }
    }

    /**
     * Look for an available framework and initialize it
     * 
     * @param pConfiguration
     *            The framework configuration
     * @return <code>true</code> if a framework has been successfully
     *         initialized, <code>false</code> otherwise.
     * @throws BundleException
     *             If an initialization error occured
     */
    private boolean initFramework(final Properties pConfiguration) throws BundleException {
        // Look for Framework factory services in the class path
        final ServiceLoader<FrameworkFactory> lLoader = ServiceLoader.<FrameworkFactory> load(
                FrameworkFactory.class, Thread.currentThread().getContextClassLoader());

        if (lLoader != null) {
            final Iterator<FrameworkFactory> lIterator = lLoader.iterator();
            if (lIterator.hasNext()) {

                // Copy properties into a map
                final Map<String, String> lConfig = new HashMap<String, String>(
                        pConfiguration.size());
                for (final Object lKey : pConfiguration.keySet()) {
                    lConfig.put(lKey.toString(), pConfiguration.getProperty(lKey.toString()));
                }

                // Create and init framework
                mFramework = lIterator.next().newFramework(lConfig);
                mFramework.init();
            }
        }
        return mFramework != null;
    }

    /**
     * Install the bundles in the specified directories.
     * 
     * @param pDirectories
     *            The directories to scan for bundles
     * @return The installed bundles
     */
    private List<Bundle> installBundles(final String pDirectories) {

        final List<Bundle> lBundles = new ArrayList<Bundle>();
        if (pDirectories != null) {

            // Collect URLs for each directory
            for (final String lDirPath : pDirectories.split(String.valueOf(','))) {

                // Install bundles
                for (final URL lUrl : scanDir(lDirPath.trim())) {
                    try {
                        lBundles.add(mFramework.getBundleContext().installBundle(lUrl.toString()));
                    } catch (final BundleException e) {
                        System.err.println(START_BUNDLE_ERROR);
                        e.printStackTrace();
                    }
                }
            }
        }
        return lBundles;
    }

    /**
     * Load properties from the main config file.
     * 
     * @return The properties from the config file
     */
    private Properties loadExternalProperties() {
        final Properties lProperties = new Properties();

        // Find and load properties file
        final String lPath = EProperties.SETTINGS_FILE.getValue(System.getProperties());
        try {

            final InputStream lStream = new FileInputStream(lPath);
            try {
                lProperties.load(lStream);
            } finally {
                lStream.close();
            }

        } catch (final IOException e) {
            System.err.println(CONFIG_FILE_ERROR + lPath);
            e.printStackTrace();
        }

        return lProperties;
    }

    /**
     * Load OSGi properties defined in the system properties.
     * 
     * @param pConfiguration
     *            The properties to fill
     */
    private void loadSystemProperties(final Properties pConfiguration) {

        for (final String lKey : System.getProperties().stringPropertyNames()) {
            if (lKey.startsWith(OSGI_PROP_NAME_ROOT) || lKey.startsWith(EProperties.NEMOS_ROOT)) {
                pConfiguration.setProperty(lKey, System.getProperty(lKey));
            }
        }
    }

    /**
     * Scan directory for JAR URLs.
     * 
     * @param pDirectory
     *            The directory to scan
     * @return The URLs of JAR files in the directory
     */
    private List<URL> scanDir(final String pDirectory) {
        final List<URL> lUrls = new ArrayList<URL>();

        final File lDir = new File(pDirectory);
        if (lDir.exists() && lDir.isDirectory()) {

            // Scan for JAR files
            for (final File lFile : lDir.listFiles()) {

                final String lUrl = lFile.toURI().toString();
                if (lFile.isFile() && lUrl.endsWith(JAR_EXTENSION)) {
                    try {
                        lUrls.add(lFile.toURI().toURL());
                    } catch (final MalformedURLException e) {
                        // Must not happen
                    }
                }
            }
        }
        return lUrls;
    }

    /**
     * Start the installed bundles.
     * 
     * @param pBundles
     *            The bundles to start
     */
    private void startBundles(final List<Bundle> pBundles) {
        for (final Bundle lBundle : pBundles) {
            try {
                // Do not start fragments
                if (lBundle.getHeaders().get(Constants.FRAGMENT_HOST) == null) {
                    lBundle.start();
                }
            } catch (final BundleException ex) {
                System.err.println(START_BUNDLE_ERROR);
                ex.printStackTrace();
            }
        }
    }
}
